﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using Liphofra.Mvvm.Validation;

namespace Liphofra.Mvvm.ViewModels
{
    /// <summary>
    /// A view model class that supports validation by implementing the <see cref="INotifyDataErrorInfo"/> interface.
    /// It assumes that derived view model classes expose properties that manipulate the underlying model instance directly,
    /// then call the <see cref="OnPropertyChanged"/> method, which in turn runs validation for the property in question. 
    /// This view model also supports adding and removing additional validation errors manually, for both individual 
    /// properties as well as for the whole model instance, by using the methods <see cref="AddExternalValidationError"/>,
    /// <see cref="ClearExternalValidationError"/> and <see cref="ClearAllExternalValidationErrors"/>.
    /// Note: by using the derived view model class itself as value for the <see cref="DataViewModelBase{T}.Model"/> property, validation
    /// can be performed on the view model level without the requirement to synchronize to any model instance.
    /// </summary>
    /// <typeparam name="T">The type of the underlying model.</typeparam>
    public abstract class ValidationViewModelBase<T> : DataViewModelBase<T>, INotifyDataErrorInfo where T : class
    {
        private IDictionary<string, IList<string>> _validationErrors;
        private IDictionary<string, string> _externalValidationErrors;
        private string _instanceError;
        private string _externalInstanceError;
        private IValidationManager _validationManager;

        private IDictionary<string, IList<string>> ValidationErrors
        {
            get
            {
                // lazy initialization
                if (_validationErrors == null)
                {
                    _validationErrors = new Dictionary<string, IList<string>>();
                }

                return _validationErrors;
            }
        }

        private IDictionary<string, string> ExternalValidationErrors
        {
            get
            {
                // lazy initialization
                if (_externalValidationErrors == null)
                {
                    _externalValidationErrors = new Dictionary<string, string>();
                }

                return _externalValidationErrors;
            }
        }

        private bool HasValidationErrors
        {
            get
            {
                return _validationErrors != null && _validationErrors.Count > 0;
            }
        }

        private bool HasExternalValidationErrors
        {
            get
            {
                return _externalValidationErrors != null && _externalValidationErrors.Count > 0;
            }
        }

        /// <summary>
        /// Gets or sets the validation manager. If no validation manager is actively set by derived classes,
        /// the service locator is used to resolve an instance of <see cref="IValidationManager"/>.
        /// </summary>
        protected IValidationManager ValidationManager
        {
            get
            {
                if (_validationManager == null)
                {
                    _validationManager = ServiceLocator.Resolve<IValidationManager>();
                }

                return _validationManager;
            }

            set
            {
                _validationManager = value;
            }
        }
        
        /// <summary>
        /// Called when a property has changed to validate the new property value on the underlying model, 
        /// to update the change tracking and raise the corresponding <see cref="ViewModelBase.PropertyChanged" /> event.
        /// Must be called by derived classes to preserve correct validation, notification and change tracking support.
        /// </summary>
        /// <param name="propertyName">The name of the property that has changed.</param>
        protected override void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            // we override this to plug in our validation logic
            ValidateProperty(propertyName);

            // ... then let the base class do its work
            base.OnPropertyChanged(propertyName);
        }

        #region IValidatingDataViewModel

        /// <summary>
        /// Determines whether the specified property name is valid.
        /// </summary>
        /// <param name="propertyName">Name of the property.</param>
        /// <returns>
        ///   <c>true</c> if the specified property name is valid; otherwise, <c>false</c>.
        /// </returns>
        public bool IsValid(string propertyName)
        {
            return !GetErrors(propertyName).OfType<string>().Any();
        }

        /// <summary>
        /// Manually validates the underlying model.
        /// </summary>
        /// <returns>
        /// A list of validation results. Can be <c>null</c> if no validation rules are found for the underlying model.
        /// </returns>
        public ValidationResults Validate()
        {
            ClearAllValidationErrors();
            var validationResults = ValidationManager.Validate(Model);
            ProcessValidationResults(validationResults);
            return validationResults;
        }

        /// <summary>
        /// Manually validates the specified property of the underlying model.
        /// </summary>
        /// <param name="propertyName">Name of the property.</param>
        /// <returns>
        /// A list of validation results. Can be <c>null</c> if no validation rules are found for the property.
        /// </returns>
        public ValidationResults ValidateProperty(string propertyName)
        {
            ClearValidationErrorsForProperty(propertyName);
            var validationResults = ValidationManager.ValidateProperty(Model, propertyName);
            ProcessValidationResults(validationResults);
            return validationResults;
        }

        /// <summary>
        /// Manually validates all properties of the underlying model.
        /// </summary>
        /// <returns>A list of validation results that contains all aggregated validation results for all properties that were validated.</returns>
        public ValidationResults ValidateAllProperties()
        {
            ClearValidationErrorsForAllProperties();
            var validationResults = ValidationManager.ValidateAllProperties(Model);
            ProcessValidationResults(validationResults);
            return validationResults;
        }

        /// <summary>
        /// Manually adds an external validation error.
        /// </summary>
        /// <param name="propertyName">The name of the property to add a validation error for. Can be <c>null</c> to add a validation error for the underlying model as a whole.</param>
        /// <param name="errorMessage">The error message to add.</param>
        public void AddExternalValidationError(string propertyName, string errorMessage)
        {
            if (string.IsNullOrEmpty(propertyName))
            {
                _externalInstanceError = errorMessage;
                OnErrorsChanged();
            }
            else
            {
                ExternalValidationErrors[propertyName] = errorMessage;
                OnErrorsChanged(propertyName);
            }
        }

        /// <summary>
        /// Clears a previously added external validation error.
        /// </summary>
        /// <param name="propertyName">The name of the property to clear the validation error from. Can be <c>null</c> to clear a validation error for the underlying model as a whole.</param>
        public void ClearExternalValidationError(string propertyName)
        {
            if (string.IsNullOrEmpty(propertyName))
            {
                _externalInstanceError = null;
                OnErrorsChanged();
            }
            else
            {
                if (HasExternalValidationErrors && ExternalValidationErrors.ContainsKey(propertyName))
                {
                    ExternalValidationErrors.Remove(propertyName);
                    OnErrorsChanged(propertyName);
                }
            }
        }

        /// <summary>
        /// Clears all previously added external validation errors.
        /// </summary>
        public void ClearAllExternalValidationErrors()
        {
            if (!string.IsNullOrWhiteSpace(_instanceError))
            {
                _externalInstanceError = null;
                OnErrorsChanged();
            }
            else
            {
                if (HasExternalValidationErrors)
                {
                    // temp store
                    var propertyNames = ExternalValidationErrors.Keys.ToList();

                    // clear
                    ExternalValidationErrors.Clear();

                    // raise changed events
                    propertyNames.ForEach(OnErrorsChanged);
                }
            }
        }

        #endregion

        #region INotifyDataErrorInfo

        /// <summary>
        /// Gets the validation errors for a specified property or for the entire entity.
        /// </summary>
        /// <param name="propertyName">The name of the property to retrieve validation errors for; or null or <see cref="F:System.String.Empty" />, to retrieve entity-level errors.</param>
        /// <returns>
        /// The validation errors for the property or entity.
        /// </returns>
        public IEnumerable GetErrors(string propertyName)
        {
            IList<string> errors = null;

            if (string.IsNullOrEmpty(propertyName))
            {
                if (_instanceError != null)
                {
                    errors = new List<string>();
                    errors.Add(_instanceError);
                }

                if (_externalInstanceError != null)
                {
                    errors = errors ?? new List<string>();
                    errors.Add(_externalInstanceError);
                }
            }
            else
            {
                if (HasValidationErrors)
                {
                    errors = ValidationErrors
                        .Where(e => e.Key.Equals(propertyName, StringComparison.OrdinalIgnoreCase))
                        .SelectMany(e => e.Value)
                        .ToList();
                }

                if (HasExternalValidationErrors)
                {
                    var externalValidationErrors = ExternalValidationErrors
                        .Where(e => e.Key.Equals(propertyName, StringComparison.OrdinalIgnoreCase))
                        .Select(e => e.Value);

                    errors = errors != null ? errors.Union(externalValidationErrors).ToList() : externalValidationErrors.ToList();
                }
            }
            
            if (errors != null)
            {
                return errors;
            }

            return Enumerable.Empty<string>();
        }

        /// <summary>
        /// Gets a value indicating whether the entity has validation errors.
        /// </summary>
        /// <returns><c>true</c> if the entity currently has validation errors; otherwise, <c>false</c>.</returns>
        public bool HasErrors
        {
            get
            {
                return HasValidationErrors || HasExternalValidationErrors || !string.IsNullOrEmpty(_instanceError) || !string.IsNullOrEmpty(_externalInstanceError);
            }
        }

        /// <summary>
        /// Occurs when the validation errors have changed for a property or for the entire entity.
        /// </summary>
        public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

        #endregion

        #region Helper Methods

        private void ClearAllValidationErrors()
        {
            if (!string.IsNullOrWhiteSpace(_instanceError))
            {
                _instanceError = null;
                OnErrorsChanged();
            }

            ClearValidationErrorsForAllProperties();
        }

        private void ClearValidationErrorsForAllProperties()
        {
            if (!HasValidationErrors)
            {
                return;
            }

            // temp store
            var propertyNames = ValidationErrors.Keys.ToList();

            // clear
            ValidationErrors.Clear();

            // raise changed event
            propertyNames.ForEach(OnErrorsChanged);
        }

        private void ClearValidationErrorsForProperty(string propertyName)
        {
            if (HasValidationErrors && ValidationErrors.ContainsKey(propertyName))
            {
                ValidationErrors.Remove(propertyName);
                OnErrorsChanged(propertyName);
            }
        }

        private void ProcessValidationResults(ValidationResults validationResults)
        {
            if (!validationResults.HasErrors)
            {
                return;
            }

            foreach (var validationResult in validationResults.Errors)
            {
                // double-check
                if (!validationResult.HasErrors)
                {
                    continue;
                }

                // build error message
                var errorMessage = string.Join(Environment.NewLine, validationResult.Errors);

                if (string.IsNullOrEmpty(validationResult.PropertyName))
                {
                    _instanceError = errorMessage;
                    OnErrorsChanged();
                }
                else
                {
                    IList<string> validationErrorsList;
                    if (!ValidationErrors.TryGetValue(validationResult.PropertyName, out validationErrorsList))
                    {
                        validationErrorsList = new List<string>();
                        ValidationErrors.Add(validationResult.PropertyName, validationErrorsList);
                    }

                    validationErrorsList.Add(errorMessage);
                    OnErrorsChanged(validationResult.PropertyName);
                }
            }
        }

        private void OnErrorsChanged(string propertyName = null)
        {
            var handlers = ErrorsChanged;
            if (handlers != null)
            {
                handlers(this, new DataErrorsChangedEventArgs(propertyName));
            }
        }

        #endregion
    }
}